﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using SourceRconLib;
using System.Net;
using System.IO;
using System.Configuration;

namespace FredBot
{
    public class GameState : IDisposable
    {
        private TFDataEntities context;
        private Match theMatch;
        private Dictionary<string, MatchTeamMember> thePlayers;
        private Dictionary<string, MatchTeamMember> theSpectators;
        private string rconPassword;
        private Rcon rconConn;
        private bool disposed = false;
        private string serverAddr;
        private int serverPort;

        public bool IsMatchStarted { get; set; }
        public String ServerIP { get; set; }
        public String MapName { get; set; }

        public GameState(string serverIP, TFDataEntities context)
        {
            this.ServerIP = serverIP;
            string[] addrAndPort = ServerIP.Split(new Char[] { ':' });
            this.serverAddr = addrAndPort[0];
            this.serverPort = Int32.Parse(addrAndPort[1]);
            this.context = context;
            thePlayers = new Dictionary<string, MatchTeamMember>();
            theSpectators = new Dictionary<string, MatchTeamMember>();
            this.rconPassword = null;
            this.rconConn = null;
        }

        private void rconConn_Errors(MessageCode code, string message)
        {
            try
            {
                File.AppendAllText("rconerrors.txt", "Received error code: " + code.ToString() + " and message: " + message + Environment.NewLine);
            }
            catch (IOException ex)
            {
                Console.WriteLine("Failed to write error to log:" + ex.Message);
            }
        }

        private void ConnectRcon()
        {
            if (rconPassword == null)
            {
                var rconQry = from s in context.Servers
                              where s.ServerIP.Equals(ServerIP)
                              select s.RconPassword;
                rconPassword = rconQry.FirstOrDefault();
            }
            if (rconPassword != null && !rconConn.Connected)
            {
                try
                {
                    rconConn.ConnectBlocking(new IPEndPoint(IPAddress.Parse(serverAddr), serverPort), rconPassword);
                }
                catch (Exception ex)
                {
                    File.AppendAllText("rconerrors.txt", "Exception thrown when connecting to " + serverAddr + ":" + serverPort + ": " + ex.Message + Environment.NewLine);
                }
            }
        }

        internal void StartMatch(LogLineData data)
        {
            IsMatchStarted = true;
            theMatch = new Match();
            theMatch.StartTime = data.Date;
            theMatch.Map = MapName;
            theMatch.ServerIP = ServerIP;
            theMatch.DemoName = StartDemo(theMatch.StartTime, theMatch.Map);
            context.Matches.AddObject(theMatch);
            context.SaveChanges();

            foreach (var curPlayer in thePlayers)
            {
                curPlayer.Value.MatchId = theMatch.Id;
                context.MatchTeamMembers.AddObject(curPlayer.Value);
            }
            context.SaveChanges();
        }

        private string StartDemo(DateTime date, string map)
        {
            ConnectRcon();
            if (rconConn.Connected)
            {
                string demoName = date.ToString("u").Replace(" ", "_").Replace(":", "-") + "_" + map;
                rconConn.ServerCommand("tv_record " + demoName);
                return demoName + ".dem";
            }

            return null;
        }

        internal void EndMatch(LogLineData data)
        {
            StopDemo();
            StartDownloadDemo(theMatch.DemoName);
            IsMatchStarted = false;
            theMatch.EndTime = data.Date;
            context.SaveChanges();

            Dictionary<string, MatchTeamMember> newPlayers = new Dictionary<string, MatchTeamMember>();
            foreach (var player in thePlayers)
            {
                newPlayers.Add(player.Key, new MatchTeamMember() { PlayerClassId = player.Value.PlayerClassId, PlayerId = player.Value.PlayerId, TeamId = player.Value.TeamId });
            }
            thePlayers = newPlayers;
        }

        internal class FtpState
        {
            public FtpWebRequest Request { get; set; }
            public string DemoName { get; set; }
            public int MatchId { get; set; }
        }

        private void StartDownloadDemo(string demoName)
        {
            if (demoName != null)
            {
                var serverQry = from s in context.Servers
                                where s.ServerIP.Equals(ServerIP)
                                select s;
                Server theServer = serverQry.FirstOrDefault();
                if (theServer != null && theServer.FtpUser != null && theServer.FtpPassword != null && theServer.TFFtpDir != null && theServer.FtpIP != null)
                {
                    Uri source = new Uri("ftp://" + theServer.FtpIP + theServer.TFFtpDir + "/" + demoName);
                    FtpWebRequest request = (FtpWebRequest)WebRequest.Create(source);
                    request.Method = WebRequestMethods.Ftp.DownloadFile;
                    request.Credentials = new NetworkCredential(theServer.FtpUser, theServer.FtpPassword);
                    request.UseBinary = true;
                    request.BeginGetResponse(new AsyncCallback(GetDemoStreamCallback), new FtpState { Request = request, DemoName = demoName, MatchId = theMatch.Id });
                }
            }
        }

        private void GetDemoStreamCallback(IAsyncResult ar)
        {
            FtpState state = (FtpState)ar.AsyncState;
            try
            {
                FtpWebResponse wr = (FtpWebResponse)state.Request.EndGetResponse(ar);
                Stream stream = wr.GetResponseStream();
                string demoDir = ConfigurationManager.AppSettings["demoDir"];
                DirectoryInfo info = new DirectoryInfo(demoDir);
                if (!info.Exists)
                {
                    info.Create();
                }
                FileStream savedDemo = new FileStream(Path.Combine(demoDir, state.DemoName), FileMode.Create);
                const int size = 8192;
                byte[] data = new byte[size];
                int numBytes = 0;
                while ((numBytes = stream.Read(data, 0, size)) > 0)
                {
                    savedDemo.Write(data, 0, numBytes);
                }
                wr.Close();
                savedDemo.Close();
                var matchQry = from m in context.Matches
                               where m.Id == state.MatchId
                               select m;
                Match finishedMatch = matchQry.FirstOrDefault();
                if (finishedMatch != null)
                {
                    finishedMatch.IsDemoComplete = true;
                    context.SaveChanges();
                }
            }
            catch (Exception ex)
            {
                File.AppendAllText("ftperrors.txt", "Failed to retrieve demo " + state.DemoName + ": " + ex.Message);
            }
        }

        private void StopDemo()
        {
            if (rconConn.Connected)
            {
                rconConn.ServerCommandBlocking("tv_stoprecord");
            }
        }

        internal void Reset(LogLineData data)
        {
            MapName = data.Noun1;

            thePlayers = new Dictionary<string, MatchTeamMember>();
            theSpectators = new Dictionary<string, MatchTeamMember>();
            if (rconConn != null)
            {
                rconConn.Dispose();
            }
            rconConn = new Rcon();
            ConnectRcon();
        }

        internal void AddPlayer(PlayerInfo pi, string className)
        {
            if (pi == null)
            {
                return;
            }

            MatchTeamMember member;
            if (!thePlayers.ContainsKey(pi.SteamID))
            {
                member = new MatchTeamMember();

                member.PlayerId = (from p in context.Players where p.SteamID.Equals(pi.SteamID) select p.Id).First();
                thePlayers.Add(pi.SteamID, member);
            }
            else
            {
                member = thePlayers[pi.SteamID];
            }
            member.TeamId = (from t in context.Teams where t.TeamName.Equals(pi.Team) select t.Id).First();
            member.PlayerClassId = (from c in context.PlayerClasses where c.ClassName.Equals(className) select c.Id).First();
        }

        internal void RecordTeamEvent(LogLineData data)
        {
            if (data.Event1.Equals("final score"))
            {
                if (data.Team.Equals("Red"))
                {
                    theMatch.RedScore = data.Noun1;
                }
                else
                {
                    theMatch.BlueScore = data.Noun1;
                }
                context.SaveChanges();
            }
            else
            {
                if (IsMatchStarted)
                {
                    LogData ld = new LogData();
                    ld.Date = data.Date;
                    ld.Team = data.Team;
                    ld.Event1 = data.Event1;
                    ld.Noun1 = data.Noun1;
                    ld.Event2 = data.Event2;
                    ld.Noun2 = data.Noun2;
                    ld.Event3 = data.Event3;
                    CommitLogDataAndProps(ld, data.Properties);
                }
            }
        }

        internal void RecordWorldEvent(LogLineData data)
        {
            if (IsMatchStarted)
            {
                if (data.Event1.StartsWith("World"))
                {
                    LogData ld = new LogData();
                    ld.Date = data.Date;
                    ld.Event1 = data.Event1;
                    ld.Noun1 = data.Noun1;
                    ld.Event2 = data.Event2;
                    ld.Noun2 = data.Noun2;
                    CommitLogDataAndProps(ld, data.Properties);
                }
            }
        }

        internal void RecordPlayerEvent(LogLineData data, PlayerInfo pi)
        {
            if (IsMatchStarted)
            {
                if (data.Event1.Equals("committed suicide with"))
                {
                    LogData ld = new LogData();
                    ld.Date = data.Date;
                    ld.Player = pi.SteamID;
                    ld.Event1 = data.Event1;
                    ld.Noun1 = data.Noun1;
                    CommitLogDataAndProps(ld, data.Properties);
                }
            }
        }

        internal void CommitLogDataAndProps(LogData ld, Dictionary<string, string> props)
        {
            ld.MatchId = theMatch.Id;
            context.LogDatas.AddObject(ld);
            context.SaveChanges();

            foreach (var kv in props)
            {
                Property p = new Property();
                p.LogDataId = ld.Id;
                p.Key = kv.Key;
                p.Value = kv.Value;
                context.Properties.AddObject(p);
            }
            context.SaveChanges();
        }

        internal void RecordPlayerPlayerEvent(LogLineData data, PlayerInfo playerInfo)
        {
            if (IsMatchStarted)
            {
                LogData ld = new LogData();
                ld.Date = data.Date;
                ld.Player = playerInfo.SteamID;
                ld.Event1 = data.Event1;
                ld.Noun1 = data.Noun1;
                ld.Event2 = data.Event2;
                ld.Noun2 = data.Noun2;
                CommitLogDataAndProps(ld, data.Properties);
            }
        }

        internal void UpdatePlayerTeam(PlayerInfo playerInfo, string team)
        {
            if (playerInfo == null)
            {
                return;
            }
            string steamID = playerInfo.SteamID;

            if (thePlayers.ContainsKey(steamID))
            {
                if (team.Equals("Spectator"))
                {
                    MatchTeamMember spec = thePlayers[steamID];
                    thePlayers.Remove(steamID);
                    theSpectators.Add(steamID, spec);
                }
                else
                {
                    thePlayers[steamID].TeamId = (from t in context.Teams where t.TeamName.Equals(team) select t.Id).First();
                }
            }
            else
            {
                if (theSpectators.ContainsKey(steamID))
                {
                    thePlayers.Add(steamID, new MatchTeamMember());
                    thePlayers[steamID].PlayerId = theSpectators[steamID].PlayerId;
                    thePlayers[steamID].PlayerClassId = theSpectators[steamID].PlayerClassId;
                    thePlayers[steamID].TeamId = (from t in context.Teams where t.TeamName.Equals(team) select t.Id).First();
                    theSpectators.Remove(steamID);
                }
            }
        }

        internal void RemovePlayer(LogLineData data, PlayerInfo pi)
        {
            if (pi == null)
                return;

            if (thePlayers.ContainsKey(pi.SteamID))
            {
                thePlayers.Remove(pi.SteamID);
            }

            if (theSpectators.ContainsKey(pi.SteamID))
            {
                theSpectators.Remove(pi.SteamID);
            }
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    if(rconConn != null)
                        rconConn.Dispose();
                }
                disposed = true;
            }
        }
    }
}